Tuesday, 08 September 2015
Functional Map API: Working with multiple entries
We continue with the blog series on the experimental Functional Map API which was released as part of Infinispan 8.0.0.Final. In this blog post we’ll be focusing on how to work with multiple entries at the same time. For reference, here are the previous entries in the series:
The approach taken by the Functional Map API when working with multiple keys is to provide a lazy, pull-style API. All multi-key operations take a collection parameter which indicates the keys to work with (and sometimes contain value information too), and a function to execute for each key/value pair. Each function’s ability depends on the entry view received as function parameter, which changes depending on the underlying map: ReadEntryView for ReadOnlyMap, WriteEntryView for WriteOnlyMap, or ReadWriteView for ReadWriteMap. The return type for all multi-key operations, except the ones from WriteOnlyMap, return an instance of Traversable which exposes methods for working with the returned data from each function execution. Let’s see an example:
This example demonstrates some of the key aspects of working with multiple entries using the Functional Map API:
-
As explained in the previous blog post, all data-handling methods (including multi-key methods) for WriteOnlyMap return CompletableFuture<Void>, because there’s nothing the function can provide that could not be computed in advance or outside the function.
-
Normally, the order of the Traversable matches the order of the input collection though this is not currently guaranteed.
There is a special type of multi-key operations which work on all keys/entries stored in Infinispan. The behaviour is very similar to the multi-key operations shown above, with the exception that they do not take a collection of keys (and/or values) as parameters:
There’s a few interesting things to note about working with all entries using the Functional Map API:
-
When working with all entries, the order of the Traversable is not guaranteed.
-
Read-only’s keys() and entries() offer the possibility to traverse all keys and entries present in the cache. When traversing entries, both keys and values including metadata are available. Contrary to Java’s ConcurrentMap, there’s no possibility to navigate only the values (and metadata) since there’s little to be gained from such method and once a key’s entry has been retrieved, there’s no extra cost to provide the key as well.
It’s worth noting that when we sat down to think about how to work with multiple entries, we considered having a push-style API where the user would receive callbacks pushed as the entries to work with were located. This is the approach that reactive APIs such as Rx follow, but we decided against using such APIs at this level for several reasons:
-
We have huge interest in providing a Rx-style API for Infinispan, but we didn’t want the core API to have a dependency on Rx or Reactive Streams.
-
We didn’t want to reimplement a push-style async API since this is not trivial to do and requires careful thinking, specially around back-pressure and flow control.
-
Push-style APIs require more work on the user side compared to pull-style APIs.
-
Pull-style APIs can still be lazy and partly asynchronous since the user can decide to work with the Traversable at a later stage, and the separation between intermediate and terminating operations provides a good abstraction to avoid unnecessary computation.
In fact, it is this desire to keep a clear separation between intermediate and terminating operations at Traversable that has resulted in having no manual way to iterate over the Traversable. In other words, there is no iterator() nor spliterator() methods in Traversable since these are often associated with manual, user-end iteration, and we want to avoid such thing since in the majority of cases, Infinispan knows best how to exactly iterate over the data.
In the next blog post, we’ll be looking at how to work with listeners using the Functional Map API.
Cheers, Galder
Tags: functional API lambda
Friday, 21 August 2015
New Functional Map API in Infinispan 8 - Introduction
In Infinispan 8.0.0.Beta3, we have a introduced a new experimental API for interacting with your data which takes advantage of the functional programming additions and improved asynchronous programming capabilities available in Java 8.
Over the next few weeks we’ll be introducing different aspects of the API. In this first blog post, we’ll focus on why we felt there’s a need for a new approach, answering a few key questions.
ConcurrentMap and JCache
Map-like key/value pair APIs have often been used for distributed caching and in-memory data grids. Initially, ConcurrentMap became popular but this was designed to be run within a single JVM, and hence some of the operations suffered in distributed environments or when persistence stores were attached. For example, methods such as 'https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#put-K-V-[V put(K, V)]', 'https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentMap.html#putIfAbsent-K-V-[V putIfAbsent(K, V)]', 'https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentMap.html#replace-K-V-[V replace(K, V)]' would force implementations to return the previous value, but often this value is not needed yet this could be expensive to transfer.
JSR-107 set out to improve on this and came up with the JCache specification which solved this particular problem separating operations such ConcurrentMap’s 'https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#put-K-V-[V put(K, V)]' into two operations: 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L194[void put(K, V)]' and 'V getAndPut(K, V)', and it applied the same logic to other operations such as 'replace' by providing an alternative 'getAndReplace(K, V)'… etc.
However, even though JCache was designed with distributed caching in mind, it still failed to provide an API to execute operations asynchronously and hence avoid resource underutilization by having threads waiting for remote operations to complete. 'loadAll' is probably the only exception, and it would have been the perfect candidate to return a Future or similar construct, but having to pass in a completion listener feels a bit clunky and cannot be chained easily.
In my opinion, the best parts of JCache are 'invoke' and 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L599[invokeAll]' methods. When you look at them, you see a lot of potential to reimplement get, put, getAndPut, getAndReplace, putAll, getAll, and many others using these methods. In other words, as an implementer, all you should need to implement is those two functions, and the rest would be syntactic sugar for the user. Unfortunately, the way 'invoke' and 'invokeAll' handle arguments is a bit clunky, and really, it’s just screaming for lambdas to be passed in and CompletableFuture instances to be returned (Java 8!).
So, when Infinispan moved to Java 8, we decided to revisit these concepts and see if we could come up with a better, distilled map-like interface to be used for as either a caching or data grid API.
New Functional Map API
Infinispan’s Functional Map API is a distilled maplike asynchronous API which uses lambdas to interact with data.
Asynchronous and Lazy
Being an asynchronous API, all methods that return a single result, return a CompletableFuture which wraps the result, so you can use the resources of your system more efficiently by having the possibility to receive callbacks when the CompletableFuture has completed, or you can chain or compose them with other CompletableFuture. If you do want to block the thread and wait for the result, just as it happens with a ConcurrentMap or JCache method call, you can simply call CompletableFuture.get()
(for such situations, we are working on finding ways to avoid unnecessary thread creation when the caller will block on the CompletableFuture).
For those operations that return multiple results, the API returns instances of a https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/Traversable.java[Traversable] interface which offers a lazy pull-style API for working with multiple results. Although push-style interfaces for handling multiple results, such as RxJava, are fully asynchronous, they’re harder to use from a user’s perspective. Traversable, being a lazy pull-style API, can still be asynchronous underneath since the user can decide to work on the traversable at a later stage, and the Traversable implementation itself can decide when to compute those results.
Lambda transparency
Since the content of the lambdas is transparent to Infinispan, the API has been split into 3 interfaces for read-only (ReadOnlyMap), read-write (ReadWriteMap) and write-only (WriteOnlyMap) operations respectively, in order to provide hints to the Infinispan internals on the type of work needed to support lambdas.
For example, Infinispan has been designed in such way that our 'ConcurrentMap.get()' and 'JCache.getAll()' implementations do not require locks to be acquired. These get()/getAll() operations are read-only operations, and hence if you call our functional map ReadOnlyMap’s 'eval()' or 'evalMany()' operations, you get the same benefit. A key advantage of ReadOnlyMap’s 'eval()' and 'evalMany()' operations is that they take lambdas as parameters which means the returned types are more flexible, so we can return a value associated with the key, or we can return a boolean if a value has the expected contents, or we can return some metadata parameters from it, e.g. last accessed time, last modified time, creation time, lifespan, version information…etc.
Another important hint that is required to make efficient use of the system is to know when a write-only operation is being executed. Write-only operations require locks to be acquired and as demonstrated by JCache’s 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L505[void removeAll()]' and `void put(K, V)' or ConcurrentMap’s 'https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#putAll-java.util.Map-[putAll()]', they do not require the previous value to be queried or read, which as explained above is a very important optimization since reading the previous value might require the persistence layer or a remote node to be queried. WriteOnlyMap’s 'https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/FunctionalMap.java#L281[eval()]', 'https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/FunctionalMap.java#L351[evalMany()]', and 'https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/FunctionalMap.java#L414[evalAll()]' follow this same pattern with the added flexibility for the lambda to decide what kind of write operation to execute.
The final type of operations we have are read-write operations, and within this category we find CAS-like (Compare-And-Swap) operations. This type of operations require previous value associated with the key to be read and for locks to be acquired before executing the lambda. Most of the operations in ConcurrentMap and JCache operations fall within this domain including: 'V put(K, V)', 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L283[boolean putIfAbsent(K, V)]', 'V replace(K, V)', 'boolean replace(K, V, V)'…etc. ReadWriteMap’s 'eval()', 'evalMany()' and 'evalAll()' provide a way to implement the vast majority of these operations thanks to the flexibility of the lambdas passed in. So you can make CAS-like comparisons not only based on value equality but based on metadata parameter equality such as version information, and you can send back previous value or boolean instances to signal whether the CAS-like comparison succeeded.
$DEITY, I need to learn a new API!!!
This new functional Map-like API is meant to complement existing Key/Value Infinispan API offerings, so you’ll still be able to use ConcurrentMap or JCache standard APIs if that’s what suits your use case best.
The target audience for this new API is either:
-
Distributed or persistent caching/in-memory data-grid users that want to benefit from CompletableFuture and/or Traversable for async/lazy data grid or caching data manipulation. The clear advantage here is that threads do not need to be idle waiting for remote operations to complete, but instead these can be notified when remote operations complete and then chain them with other subsequent operations.
-
Users wanting to go beyond the standard operations exposed by ConcurrentMap and JCache, for example, if you want to do a replace operation using metadata parameter equality instead of value equality, or if you want to retrieve metadata information from values…etc.
Internally, we feel that this new functional Map-like API distills the Map-like APIs that we currently offer (including ConcurrentMap and JCache) and gets rid of a lot of duplication in our AdvancedCache API (e.g. 'https://docs.jboss.org/infinispan/8.0/apidocs/org/infinispan/AdvancedCache.html#getCacheEntry-java.lang.Object-[getCacheEntry()]', 'https://docs.jboss.org/infinispan/8.0/apidocs/org/infinispan/commons/api/AsyncCache.html#getAsync-K-[getAsync()]', 'https://docs.jboss.org/infinispan/8.0/apidocs/org/infinispan/commons/api/AsyncCache.html#putAsync-K-V-[putAsync()]', 'put(K, V, Metadata)'…etc), and hence down the line, we’d want all these APIs to be implemented using the new functional Maplike API. By doing that, we hope to reduce the number of commands that our internal architecture implements, hence reducing our code base.
This new API also offers a new approach for passing per-invocation parameters, and much more flexible Metadata handling compared to our current approach. As we dig into this new API in next blog posts, we’ll explain the differences and advantages provided by these.
Functional Map API usage examples
To give you a little taste of what the API looks like, here is a write-only operation to associate a key with a value, whose CompletableFuture has been chained so that when it completes, a read-only operation can be executed to read the stored value, and when that completes, print it to the system output:
You can find more examples of this new API in FunctionalConcurrentMap and FunctionalJCache classes, which are implementations of ConcurrentMap and JCache respectively using the new Functional Map API.
Tell me more!!
Over the next few weeks I’ll be posting examples looking at the finer details of these new Functional Map APIs, but if you’re eager to get started, check the classes in org.infinispan.functional package, FunctionalConcurrentMap and FunctionalJCache which are ConcurrentMap and JCache implementations based on these Functional Map APIs, and FunctionalMapTest which demonstrates operations that go beyond what ConcurrentMap and JCache offer.
Happy (functional) hacking :)
Galder
Tags: functional introduction API lambda